Add a cargo-doc command
authorAlex Crichton <alex@alexcrichton.com>
Tue, 22 Jul 2014 15:06:36 +0000 (08:06 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Mon, 28 Jul 2014 02:17:26 +0000 (19:17 -0700)
15 files changed:
Cargo.toml
src/bin/cargo-doc.rs [new file with mode: 0644]
src/bin/cargo.rs
src/cargo/core/manifest.rs
src/cargo/ops/cargo_clean.rs
src/cargo/ops/cargo_compile.rs
src/cargo/ops/cargo_doc.rs [new file with mode: 0644]
src/cargo/ops/cargo_rustc/context.rs
src/cargo/ops/cargo_rustc/fingerprint.rs
src/cargo/ops/cargo_rustc/layout.rs
src/cargo/ops/cargo_rustc/mod.rs
src/cargo/ops/mod.rs
src/cargo/util/toml.rs
tests/test_cargo_doc.rs [new file with mode: 0644]
tests/tests.rs

index e13a0ee624e80901a8744f8f71a46496904d0a3a..e1f1d190ddf0835a289447d69a971b30829ffdb9 100644 (file)
@@ -67,5 +67,9 @@ test = false
 name = "cargo-new"
 test = false
 
+[[bin]]
+name = "cargo-doc"
+test = false
+
 [[test]]
 name = "tests"
diff --git a/src/bin/cargo-doc.rs b/src/bin/cargo-doc.rs
new file mode 100644 (file)
index 0000000..e61c0dd
--- /dev/null
@@ -0,0 +1,62 @@
+#![feature(phase)]
+
+#[phase(plugin, link)]
+extern crate cargo;
+extern crate serialize;
+
+#[phase(plugin, link)]
+extern crate hammer;
+
+use std::os;
+
+use cargo::ops;
+use cargo::{execute_main_without_stdin};
+use cargo::core::{MultiShell};
+use cargo::util::{CliResult, CliError};
+use cargo::util::important_paths::find_project_manifest;
+
+#[deriving(PartialEq,Clone,Decodable)]
+struct Options {
+    manifest_path: Option<String>,
+    jobs: Option<uint>,
+    update: bool,
+    no_deps: bool,
+}
+
+hammer_config!(Options "Build the package's documentation", |c| {
+    c.short("jobs", 'j').short("update", 'u')
+})
+
+fn main() {
+    execute_main_without_stdin(execute);
+}
+
+fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
+    let root = match options.manifest_path {
+        Some(path) => Path::new(path),
+        None => try!(find_project_manifest(&os::getcwd(), "Cargo.toml")
+                    .map_err(|_| {
+                        CliError::new("Could not find Cargo.toml in this \
+                                       directory or any parent directory",
+                                      102)
+                    }))
+    };
+
+    let mut doc_opts = ops::DocOptions {
+        all: !options.no_deps,
+        compile_opts: ops::CompileOptions {
+            update: options.update,
+            env: if options.no_deps {"doc"} else {"doc-all"},
+            shell: shell,
+            jobs: options.jobs,
+            target: None,
+        },
+    };
+
+    try!(ops::doc(&root, &mut doc_opts).map_err(|err| {
+        CliError::from_boxed(err, 101)
+    }));
+
+    Ok(None)
+}
+
index 824daae8fc275b4bfe27497ede736033813190e3..fe3a61aba1d61970be16b8e7ce0e8c1649a1c60a 100644 (file)
@@ -57,6 +57,7 @@ fn execute() {
             println!("  run            # build and execute src/main.rs");
             println!("  version        # displays the version of cargo");
             println!("  new            # create a new cargo project");
+            println!("  doc            # build project's rustdoc documentation");
             println!("");
 
 
index fa752fa07c8cd19ad6588c32668a74bbb71aa5f9..afe616848ef8cd335a1e6b04ec5483bde6070621 100644 (file)
@@ -19,6 +19,7 @@ pub struct Manifest {
     authors: Vec<String>,
     targets: Vec<Target>,
     target_dir: Path,
+    doc_dir: Path,
     sources: Vec<SourceId>,
     build: Vec<String>,
     unused_keys: Vec<String>,
@@ -41,6 +42,7 @@ pub struct SerializedManifest {
     authors: Vec<String>,
     targets: Vec<Target>,
     target_dir: String,
+    doc_dir: String,
     build: Option<Vec<String>>,
 }
 
@@ -55,6 +57,7 @@ impl<E, S: Encoder<E>> Encodable<S, E> for Manifest {
             authors: self.authors.clone(),
             targets: self.targets.clone(),
             target_dir: self.target_dir.display().to_string(),
+            doc_dir: self.doc_dir.display().to_string(),
             build: if self.build.len() == 0 { None } else { Some(self.build.clone()) },
         }.encode(s)
     }
@@ -155,10 +158,25 @@ impl Profile {
         }
     }
 
+    pub fn default_doc() -> Profile {
+        Profile {
+            env: "doc".to_string(),
+            opt_level: 0,
+            debug: false,
+            test: false,
+            dest: Some("doc-build".to_string()),
+            plugin: false,
+        }
+    }
+
     pub fn is_compile(&self) -> bool {
         self.env.as_slice() == "compile"
     }
 
+    pub fn is_doc(&self) -> bool {
+        self.env.as_slice() == "doc"
+    }
+
     pub fn is_test(&self) -> bool {
         self.test
     }
@@ -249,13 +267,14 @@ impl Show for Target {
 
 impl Manifest {
     pub fn new(summary: &Summary, targets: &[Target],
-               target_dir: &Path, sources: Vec<SourceId>,
+               target_dir: &Path, doc_dir: &Path, sources: Vec<SourceId>,
                build: Vec<String>) -> Manifest {
         Manifest {
             summary: summary.clone(),
             authors: Vec::new(),
             targets: Vec::from_slice(targets),
             target_dir: target_dir.clone(),
+            doc_dir: doc_dir.clone(),
             sources: sources,
             build: build,
             unused_keys: Vec::new(),
@@ -294,6 +313,10 @@ impl Manifest {
         &self.target_dir
     }
 
+    pub fn get_doc_dir(&self) -> &Path {
+        &self.doc_dir
+    }
+
     pub fn get_source_ids(&self) -> &[SourceId] {
         self.sources.as_slice()
     }
index 35ec741f5b2b1eaa3ec3dc87631098ae61a87def..434bb65e26b74aa45248db739496d2573a91c78a 100644 (file)
@@ -1,25 +1,30 @@
 use std::io::fs::{rmdir_recursive};
-use core::{SourceId};
+
+use core::source::Source;
+use sources::PathSource;
 use util::{CargoResult, human, ChainError};
-use ops::{read_manifest};
-use std::io::{File};
-use util::toml::{project_layout};
 
 /// Cleans the project from build artifacts.
 
 pub fn clean(manifest_path: &Path) -> CargoResult<()> {
-    let mut file = try!(File::open(manifest_path));
-    let data = try!(file.read_to_end());
-    let layout = project_layout(&manifest_path.dir_path());
-    let (manifest, _) = try!(read_manifest(data.as_slice(), 
-                                           layout, 
-                                           &SourceId::for_path(manifest_path)));
+    let mut src = PathSource::for_path(&manifest_path.dir_path());
+    try!(src.update());
+    let root = try!(src.get_root_package());
+    let manifest = root.get_manifest();
 
     let build_dir = manifest.get_target_dir();
-
     if build_dir.exists() {
-      rmdir_recursive(build_dir).chain_error(|| human("Could not remove build directory"))
-    } else {
-      Ok(())
+        try!(rmdir_recursive(build_dir).chain_error(|| {
+            human("Could not remove build directory")
+        }))
     }
+
+    let doc_dir = manifest.get_doc_dir();
+    if doc_dir.exists() {
+        try!(rmdir_recursive(doc_dir).chain_error(|| {
+            human("Could not remove documentation directory")
+        }))
+    }
+
+    Ok(())
 }
index 966848d444f5ba6f3075f9e8f72643acf270f724..d746901f4372002431765e11198b9d7c876c9fca 100644 (file)
@@ -85,7 +85,11 @@ pub fn compile(manifest_path: &Path,
     debug!("packages={}", packages);
 
     let targets = package.get_targets().iter().filter(|target| {
-        target.get_profile().get_env() == env
+        match env {
+            // doc-all == document everything, so look for doc targets
+            "doc" | "doc-all" => target.get_profile().get_env() == "doc",
+            env => target.get_profile().get_env() == env,
+        }
     }).collect::<Vec<&Target>>();
 
     let mut config = try!(Config::new(*shell, update, jobs, target));
diff --git a/src/cargo/ops/cargo_doc.rs b/src/cargo/ops/cargo_doc.rs
new file mode 100644 (file)
index 0000000..f59bf9b
--- /dev/null
@@ -0,0 +1,22 @@
+use std::io::fs;
+
+use ops;
+use util::CargoResult;
+use core::source::Source;
+use sources::PathSource;
+
+pub struct DocOptions<'a> {
+    pub all: bool,
+    pub compile_opts: ops::CompileOptions<'a>,
+}
+
+pub fn doc(manifest_path: &Path,
+           options: &mut DocOptions) -> CargoResult<()> {
+    let mut src = PathSource::for_path(&manifest_path.dir_path());
+    try!(src.update());
+    let root = try!(src.get_root_package());
+    let output = root.get_manifest().get_target_dir().join("doc");
+    let _ = fs::rmdir_recursive(&output);
+    try!(ops::compile(manifest_path, &mut options.compile_opts));
+    Ok(())
+}
index 8ccd14bc7f24936bab4e69f16d27008c84fc0869..c9627814719efcce4da91bfd1fc04c16df57bfeb 100644 (file)
@@ -216,7 +216,11 @@ impl<'a, 'b> Context<'a, 'b> {
 
     pub fn is_relevant_target(&self, target: &Target) -> bool {
         target.is_lib() && match self.env {
-            "test" => target.get_profile().is_compile(),
+            "doc" | "test" => target.get_profile().is_compile(),
+            // doc-all == document everything, so look for doc targets and
+            //            compile targets in dependencies
+            "doc-all" => target.get_profile().is_compile() ||
+                         target.get_profile().is_doc(),
             _ => target.get_profile().get_env() == self.env,
         }
     }
index 6d2c0f3d94f5e769a169ebaabda0372f1899970e..81520a904173ee3c7be6a6a1cf162c62b7774cba 100644 (file)
@@ -56,6 +56,7 @@ pub fn prepare(cx: &mut Context, pkg: &Package,
     }
 
     for &target in targets.iter() {
+        if target.get_profile().is_doc() { continue }
         let layout = cx.layout(target.get_profile().is_plugin());
         for filename in cx.target_filenames(target).iter() {
             let filename = filename.as_slice();
index 42bf4a0f859867b26d9251012b65b4164e384608..8c06656ba6dc4d07df36bd41d11c9e851eb8316d 100644 (file)
@@ -153,4 +153,6 @@ impl<'a> LayoutProxy<'a> {
     pub fn old_native(&self, pkg: &Package) -> Path {
         self.root.old_native(pkg)
     }
+
+    pub fn proxy(&self) -> &'a Layout { self.root }
 }
index c215c181a5fba6cbe8ca6c2872da28203a87e28a..9c5535b155f45ad1f3e1b34d5e011d3113b8bce8 100644 (file)
@@ -110,8 +110,12 @@ fn compile<'a, 'b>(targets: &[&'a Target], pkg: &'a Package,
     // interdependencies.
     let (mut libs, mut bins) = (Vec::new(), Vec::new());
     for &target in targets.iter() {
-        let req = cx.get_requirement(pkg, target);
-        let jobs = rustc(pkg, target, cx, req);
+        let jobs = if target.get_profile().is_doc() {
+            vec![rustdoc(pkg, target, cx)]
+        } else {
+            let req = cx.get_requirement(pkg, target);
+            rustc(pkg, target, cx, req)
+        };
         if target.is_lib() {
             libs.push_all_move(jobs);
         } else {
@@ -222,6 +226,44 @@ fn prepare_rustc(package: &Package, target: &Target, crate_types: Vec<&str>,
     }
 }
 
+
+fn rustdoc(package: &Package, target: &Target, cx: &mut Context) -> Job {
+    // Can't document binaries, but they have a doc target listed so we can
+    // build documentation of dependencies even when `cargo doc` is run.
+    if target.is_bin() {
+        return Job::new(proc() Ok(Vec::new()))
+    }
+
+    let pkg_root = package.get_root();
+    let cx_root = cx.layout(false).proxy().dest().dir_path().join("doc");
+    let rustdoc = util::process("rustdoc").cwd(pkg_root.clone());
+    let rustdoc = rustdoc.arg(target.get_src_path())
+                         .arg("-o").arg(cx_root)
+                         .arg("--crate-name").arg(target.get_name());
+    let rustdoc = build_deps_args(rustdoc, target, package, cx, false);
+
+    log!(5, "commands={}", rustdoc);
+
+    let _ = cx.config.shell().verbose(|shell| {
+        shell.status("Running", rustdoc.to_string())
+    });
+
+    let primary = cx.primary;
+    let name = package.get_name().to_string();
+    Job::new(proc() {
+        if primary {
+            try!(rustdoc.exec().chain_error(|| {
+                human(format!("Could not document `{}`.", name))
+            }))
+        } else {
+            try!(rustdoc.exec_with_output().and(Ok(())).map_err(|err| {
+                caused_human(format!("Could not document `{}`.\n{}",
+                                     name, err.output().unwrap()), err)
+            }))
+        }
+        Ok(Vec::new())
+    })
+}
 fn build_base_args(mut cmd: ProcessBuilder,
                    target: &Target,
                    crate_types: &[&str]) -> ProcessBuilder {
index 5c16029fe2b92f5f0d9a99e3cdf125b30d5efecc..392b364214756bc4499795ee9f63347dc67e2ff2 100644 (file)
@@ -4,6 +4,7 @@ pub use self::cargo_read_manifest::{read_manifest,read_package,read_packages};
 pub use self::cargo_rustc::compile_targets;
 pub use self::cargo_run::run;
 pub use self::cargo_new::{new, NewOptions};
+pub use self::cargo_doc::{doc, DocOptions};
 
 mod cargo_clean;
 mod cargo_compile;
@@ -11,3 +12,4 @@ mod cargo_read_manifest;
 mod cargo_rustc;
 mod cargo_run;
 mod cargo_new;
+mod cargo_doc;
index 45dfacb1d236fd337ec79ddf8a5d9ebc4cee50cf..ad55d640ab1e18b86717e01887f6291037d566cf 100644 (file)
@@ -234,6 +234,7 @@ fn inferred_lib_target(name: &str, layout: &Layout) -> Vec<TomlTarget> {
             path: Some(TomlPath(lib.clone())),
             test: None,
             plugin: None,
+            doc: None,
         }]
     }).unwrap_or(Vec::new())
 }
@@ -254,6 +255,7 @@ fn inferred_bin_targets(name: &str, layout: &Layout) -> Vec<TomlTarget> {
                 path: Some(TomlPath(bin.clone())),
                 test: None,
                 plugin: None,
+                doc: None,
             }
         })
     }).collect()
@@ -268,6 +270,7 @@ fn inferred_example_targets(layout: &Layout) -> Vec<TomlTarget> {
                 path: Some(TomlPath(ex.clone())),
                 test: None,
                 plugin: None,
+                doc: None,
             }
         })
     }).collect()
@@ -282,6 +285,7 @@ fn inferred_test_targets(layout: &Layout) -> Vec<TomlTarget> {
                 path: Some(TomlPath(ex.clone())),
                 test: None,
                 plugin: None,
+                doc: None,
             }
         })
     }).collect()
@@ -316,6 +320,7 @@ impl TomlManifest {
                         path: layout.lib.as_ref().map(|p| TomlPath(p.clone())),
                         test: t.test,
                         plugin: t.plugin,
+                        doc: t.doc,
                     }
                 } else {
                     t.clone()
@@ -336,6 +341,7 @@ impl TomlManifest {
                         path: bin.as_ref().map(|&p| TomlPath(p.clone())),
                         test: t.test,
                         plugin: None,
+                        doc: t.doc,
                     }
                 } else {
                     t.clone()
@@ -387,6 +393,7 @@ impl TomlManifest {
                 &summary,
                 targets.as_slice(),
                 &Path::new("target"),
+                &Path::new("doc"),
                 sources,
                 match project.build {
                     Some(SingleBuildCommand(ref cmd)) => vec!(cmd.clone()),
@@ -454,6 +461,7 @@ struct TomlTarget {
     crate_type: Option<Vec<String>>,
     path: Option<TomlPath>,
     test: Option<bool>,
+    doc: Option<bool>,
     plugin: Option<bool>,
 }
 
@@ -491,8 +499,7 @@ fn normalize(libs: &[TomlLibTarget],
 
     enum TestDep { Needed, NotNeeded }
 
-    fn target_profiles(target: &TomlTarget,
-                       dep: TestDep) -> Vec<Profile> {
+    fn target_profiles(target: &TomlTarget, dep: TestDep) -> Vec<Profile> {
         let mut ret = vec![Profile::default_dev(), Profile::default_release()];
 
         match target.test {
@@ -500,6 +507,11 @@ fn normalize(libs: &[TomlLibTarget],
             Some(false) => {}
         }
 
+        match target.doc {
+            Some(true) | None => ret.push(Profile::default_doc()),
+            Some(false) => {}
+        }
+
         match dep {
             Needed => ret.push(Profile::default_test().test(false)),
             _ => {}
diff --git a/tests/test_cargo_doc.rs b/tests/test_cargo_doc.rs
new file mode 100644 (file)
index 0000000..6ca18ab
--- /dev/null
@@ -0,0 +1,202 @@
+use support::{project, execs};
+use support::{COMPILING};
+use hamcrest::{assert_that, existing_file, existing_dir, is_not};
+
+fn setup() {
+}
+
+test!(simple {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("src/lib.rs", r#"
+            pub fn foo() {}
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+    assert_that(&p.root().join("target/doc"), existing_dir());
+    assert_that(&p.root().join("target/doc/foo/index.html"), existing_file());
+})
+
+test!(no_build_main {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("src/lib.rs", r#"
+            pub fn foo() {}
+        "#)
+        .file("src/main.rs", r#"
+            bad code
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+})
+
+test!(doc_no_libs {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("src/main.rs", r#"
+            bad code
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0));
+})
+
+test!(doc_twice {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("src/lib.rs", r#"
+            pub fn foo() {}
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+})
+
+test!(doc_deps {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies.bar]
+            path = "bar"
+        "#)
+        .file("src/lib.rs", r#"
+            extern crate bar;
+            pub fn foo() {}
+        "#)
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("bar/src/lib.rs", r#"
+            pub fn bar() {}
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} bar v0.0.1 (file:{dir})
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+
+    assert_that(&p.root().join("target/doc"), existing_dir());
+    assert_that(&p.root().join("target/doc/foo/index.html"), existing_file());
+    assert_that(&p.root().join("target/doc/bar/index.html"), existing_file());
+})
+
+test!(doc_no_deps {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies.bar]
+            path = "bar"
+        "#)
+        .file("src/lib.rs", r#"
+            extern crate bar;
+            pub fn foo() {}
+        "#)
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("bar/src/lib.rs", r#"
+            pub fn bar() {}
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc").arg("--no-deps"),
+                execs().with_status(0).with_stdout(format!("\
+{compiling} bar v0.0.1 (file:{dir})
+{compiling} foo v0.0.1 (file:{dir})
+",
+        compiling = COMPILING,
+        dir = p.root().display()).as_slice()));
+
+    assert_that(&p.root().join("target/doc"), existing_dir());
+    assert_that(&p.root().join("target/doc/foo/index.html"), existing_file());
+    assert_that(&p.root().join("target/doc/bar/index.html"), is_not(existing_file()));
+})
+
+test!(doc_only_bin {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies.bar]
+            path = "bar"
+        "#)
+        .file("src/main.rs", r#"
+            extern crate bar;
+            pub fn foo() {}
+        "#)
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("bar/src/lib.rs", r#"
+            pub fn bar() {}
+        "#);
+
+    assert_that(p.cargo_process("cargo-doc"),
+                execs().with_status(0));
+
+    assert_that(&p.root().join("target/doc"), existing_dir());
+    assert_that(&p.root().join("target/doc/bar/index.html"), existing_file());
+})
index a77d451f2ce7b098aa33a338dc6181dc600ac21b..37a7364c6e1c8cb974aeb0471d79d8440eea215e 100644 (file)
@@ -31,3 +31,4 @@ mod test_cargo_run;
 mod test_cargo_version;
 mod test_cargo_new;
 mod test_cargo_compile_plugins;
+mod test_cargo_doc;